Skip to content

fix(engine): Jeweled Amulet notes and reproduces spent mana type (#6504) - #6812

Merged
matthewevans merged 9 commits into
phase-rs:mainfrom
jsdevninja:fix/6504-jeweled-amulet-noted-mana
Aug 8, 2026
Merged

fix(engine): Jeweled Amulet notes and reproduces spent mana type (#6504)#6812
matthewevans merged 9 commits into
phase-rs:mainfrom
jsdevninja:fix/6504-jeweled-amulet-noted-mana

Conversation

@jsdevninja

@jsdevninja jsdevninja commented Jul 30, 2026

Copy link
Copy Markdown
Contributor

Summary

Fixes #6504. Jeweled Amulet's first ability placed a charge counter but never noted the mana type spent to pay its own activation cost, so the second ability ("Add one mana of this artifact's last noted type") always produced zero mana. Both clauses were falling through to Effect::Unimplemented.

Built as a reusable primitive rather than a card-specific special case, scoped to Jeweled Amulet's exact "note the type of mana spent to pay this activation cost" / "one mana of this artifact's last noted type" wording:

  • GameObject::mana_spent_to_activate — a transient per-object latch stamped at ability-mana-cost payment time (mirrors the existing colors_spent_to_cast cast-side idiom).
  • ChosenAttribute::NotedManaSpent(Vec<ManaType>) — the durable, engine-set (never player-prompted) noted value, following the same precedent as ChosenAttribute::Card/TributeOutcome.
  • Effect::NoteManaSpent — writes the noted value at resolution, not at payment time, so a countered ability never notes anything (CR 608.2c).
  • ManaProduction::NotedType — reads it back at a companion mana ability's resolution, with CR 106.5-correct "no noted type → no mana" behavior.

Ice Cauldron's sibling wording ("note the type AND AMOUNT of mana spent...") is intentionally NOT matched. It needs an exact stored multiset plus a spend restriction ("spend this mana only to cast the last card exiled with this artifact"), which this primitive doesn't model — that card's Oracle text still falls through to Effect::Unimplemented, unchanged from before this PR.

CR 400.7 note

Effect::NoteManaSpent refuses to write unless the source's live incarnation still matches the incarnation captured when its cost was paid, so a source bounced/flickered while its own "note" ability sits unresolved on the stack can't have the departed incarnation's payment silently promoted onto the new one. Mirrors the engine's existing incarnation-pairing idiom (ResolvedAbility::source_is_current, TargetFilter::SelfRef resolution).

Test plan

  • Parser test (oracle_parser.rs) asserts Jeweled Amulet's Oracle text parses to Effect::NoteManaSpent / ManaProduction::NotedType with zero Unimplemented residue.
  • Runtime tests (issue_6504_jeweled_amulet_noted_mana.rs) drive the full activation → resolution → mana-ability pipeline:
    • notes red, produces red (not zero)
    • notes green, produces green (not hardcoded to one color)
    • CR 106.5: nothing noted → zero mana, not a default color
    • CR 400.7: source bounced through the real bounce resolver while its own note ability sits unresolved on the stack → the new incarnation notes nothing (verified revert-failing)
  • cargo fmt --all, cargo clippy --all-targets --workspace -- -D warnings, ./scripts/check-parser-combinators.sh, cargo test -p phase-engine (18k+ lib tests, 4.2k+ integration tests), cargo test -p phase-ai all green.

Summary by CodeRabbit

  • New Features

    • Cards can now record the type(s) of mana spent to activate abilities, and those noted types can drive “last noted type” mana production.
    • Added parsing and display support for “note the type of mana spent” and noted-type mana instructions.
  • Bug Fixes

    • Note recording now correctly respects resolution timing and prevents incorrect carryover across bounce/flicker/copy scenarios.
    • Improved handling so note-type effects are treated correctly by trigger indexing, coverage, polarity, and related walkers.
  • Tests

    • Expanded integration and oracle parser tests for Jeweled Amulet, including grammar variants and per-activation isolation.

@jsdevninja
jsdevninja requested a review from matthewevans as a code owner July 30, 2026 10:33
@coderabbitai

coderabbitai Bot commented Jul 30, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

Adds NoteManaSpent parsing and resolution, records activation-payment mana in per-ability snapshots, and introduces ManaProduction::NotedType. Tests cover noted-color production, absent noted mana, incarnation changes, resolution timing, replacement, stacked activations, and copied abilities.

Changes

Noted mana support

Layer / File(s) Summary
Effect and parser contracts
crates/engine/src/types/ability.rs, crates/engine/src/parser/oracle_*
Adds note-mana effect and noted-type production representations with Oracle parsing and exhaustive handling.
Payment recording and note resolution
crates/engine/src/game/casting.rs, crates/engine/src/game/casting_costs.rs, crates/engine/src/game/game_object.rs, crates/engine/src/game/effects/*
Transfers spent mana into ResolvedAbility snapshots and records it only when the source incarnation matches.
Noted-type mana production
crates/engine/src/game/effects/mana.rs, crates/engine/src/game/mana_sources.rs
Produces the recorded mana type, exposes it to mana options and display, and produces no mana when none is noted.
Analysis and compatibility classification
crates/engine/src/analysis/*, crates/engine/src/game/*, crates/mtgish-import/*, crates/phase-ai/*
Integrates the new variants into scanners, coverage, trigger indexing, batching, import rewriting, and classifiers.
Parser and gameplay regression tests
crates/engine/tests/integration/*
Tests Oracle lowering, noted red and green mana, charge removal, timing, replacement, bounced incarnations, LIFO activations, and copied abilities.

Estimated code review effort: 4 (Complex) | ~60 minutes

Sequence Diagram(s)

sequenceDiagram
  participant OracleParser
  participant CastingEngine
  participant GameObject
  participant ResolvedAbility
  participant NoteManaSpent
  participant ManaResolver

  OracleParser->>CastingEngine: lower note and NotedType abilities
  CastingEngine->>GameObject: record spent mana
  CastingEngine->>ResolvedAbility: snapshot payment and source incarnation
  ResolvedAbility->>NoteManaSpent: provide captured payment
  NoteManaSpent->>GameObject: validate incarnation and store noted mana
  ManaResolver->>GameObject: read noted mana type
  ManaResolver->>CastingEngine: produce noted mana
Loading

Possibly related PRs

Suggested labels: enhancement

Suggested reviewers: matthewevans, mcbradd, claytonlin1110, michiot05

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title is concise and accurately summarizes the main Jeweled Amulet mana-note fix.
Linked Issues check ✅ Passed The changes implement Jeweled Amulet's note-and-reproduce mana behavior, including source-incarnation safety and last-noted-type production.
Out of Scope Changes check ✅ Passed The added code stays focused on the noted-mana feature and its required test and plumbing updates.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@matthewevans matthewevans left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Changes requested — the declared noted-mana class overreaches Ice Cauldron and loses source incarnation.

🔴 Blocker

parser/oracle_effect/imperative.rs:10121-10136 deliberately accepts “note the type and amount of mana,” including Ice Cauldron, but game/effects/mana.rs:544-559 and :852-865 retain only the first noted type and repeat the printed count. The producer parser at parser/oracle_effect/mana.rs:1407-1420 recognizes only “mana of ~'s last noted type.” Ice Cauldron’s current Oracle text is: “Add this artifact's last noted type and amount of mana.” Its X payment must determine the later production amount; this implementation cannot parse or reproduce that full instruction while claiming the class supports it.

Either narrow the change and its claims to singular-type wording, keeping Ice Cauldron strict-unimplemented, or model the noted amount as typed state and add parser plus end-to-end runtime coverage for Ice Cauldron’s activation, stored amount, later production, and spend restriction.

🔴 Blocker

game/effects/note_mana_spent.rs:29-41 reads and writes the latch through raw ability.source_id, while types/ability.rs:22697-22706 provides source_incarnation specifically to prevent rebinding a departed source and game/zones.rs:161-164 keeps an ObjectId as storage identity across zone changes. The new mana_spent_to_activate field at game_object.rs:1134-1149 has no zone-exit cleanup. A bounced or flickered source can therefore receive an old activation’s note on its new incarnation. Capture paid mana and source incarnation in the resolving context, write only if the captured source remains that incarnation, and add a stack-level bounce/flicker regression.

🟡 Required evidence

This engine/parser PR has no current <!-- coverage-parse-diff --> sticky despite a completed Card data job. Publish and reconcile current-head parse-diff evidence. The Rust lint job is also terminal red; resolve its reported gate before re-review.

Recommendation: make the supported class honest or implement the missing type-and-amount and source-incarnation semantics with discriminating end-to-end tests, then regenerate the parser evidence.

@github-actions

github-actions Bot commented Jul 30, 2026

Copy link
Copy Markdown

Generated for head 2243c0834e0307f4f9c204c73cbb5f580e106e57.

Parse changes introduced by this PR · 1 card(s), 4 signature(s) (baseline: main 1833435c0d59)

🟢 Added (2 signatures)

  • 1 card · ➕ ability/Mana · added: Mana (kind=activated, mana=1 of noted type)
    • Affected (first 3): Jeweled Amulet
  • 1 card · ➕ ability/NoteManaSpent · added: NoteManaSpent
    • Affected (first 3): Jeweled Amulet

🔴 Removed (2 signatures)

  • 1 card · ➖ ability/add · removed: add (kind=activated)
    • Affected (first 3): Jeweled Amulet
  • 1 card · ➖ ability/note · removed: note
    • Affected (first 3): Jeweled Amulet

1 card(s) had Oracle-text changes (errata/reprint) — excluded as non-parser.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 5

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@crates/engine/src/game/ability_scan.rs`:
- Line 4964: Update the ManaProduction::NotedType arm in the ability scan
classification to set sibling: true while preserving its existing count scan.
Add a regression test covering fixed-count NotedType behavior when another
resolution changes the noted mana type, ensuring LoopFirewall detects the
dependency.

In `@crates/engine/src/game/casting.rs`:
- Around line 14725-14739: Move the spent-mana snapshot out of the source-global
latch and bind it to each activated ability/stack entry together with the source
incarnation. In crates/engine/src/game/casting.rs:14725-14739, capture the
payment snapshot during activation; in
crates/engine/src/game/game_object.rs:1134-1149, clear or remove transient
source-level payment state on object changes; and in
crates/engine/src/game/effects/note_mana_spent.rs:29-41, require the matching
incarnation and consume that activation’s snapshot before recording the note.
Add regression coverage for repeated activations across untap and source
re-entry before resolution.

In `@crates/engine/src/game/effects/mana.rs`:
- Around line 544-560: Update the NotedType representation and its handling in
the ManaProduction path to preserve and later reproduce the full mana
composition recorded by the noting activation, including both type and amount
for Ice Cauldron. Replace the first-type repetition and current count resolution
in the NotedType branch with consumption of the durable noted mana value, and
update related support paths around noted_mana_type_for so the reusable
abstraction is not limited to Jeweled Amulet.

In `@crates/engine/tests/integration/issue_6504_jeweled_amulet_noted_mana.rs`:
- Line 77: Update the test around runner.activate and resolve to separate
activation from resolution, then assert that no durable noted mana type is
usable while the activated ability remains unresolved. Ensure the regression
reaches the production mana-payment pipeline and verifies the noted type becomes
available only after NoteManaSpent resolves.
- Around line 127-155: Update jeweled_amulet_tracks_a_different_noted_color to
use the same amulet for two complete note/consume cycles: first note red and
consume the resulting ability, then note green and consume it again through the
existing runner production pipeline. Assert the second cycle produces green and
no red, ensuring the stored noted type is replaced rather than appended and read
stale.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 1ae99cad-6b87-4655-9932-5ab54062d9d3

📥 Commits

Reviewing files that changed from the base of the PR and between 896f0aa and a767997.

📒 Files selected for processing (29)
  • crates/engine/src/analysis/ability_graph.rs
  • crates/engine/src/game/ability_rw.rs
  • crates/engine/src/game/ability_scan.rs
  • crates/engine/src/game/casting.rs
  • crates/engine/src/game/casting_costs.rs
  • crates/engine/src/game/coverage.rs
  • crates/engine/src/game/effects/mana.rs
  • crates/engine/src/game/effects/mod.rs
  • crates/engine/src/game/effects/note_mana_spent.rs
  • crates/engine/src/game/game_object.rs
  • crates/engine/src/game/mana_sources.rs
  • crates/engine/src/game/printed_cards.rs
  • crates/engine/src/game/trigger_index.rs
  • crates/engine/src/parser/oracle_effect/imperative.rs
  • crates/engine/src/parser/oracle_effect/mana.rs
  • crates/engine/src/parser/oracle_effect/mod.rs
  • crates/engine/src/parser/oracle_effect/sequence.rs
  • crates/engine/src/parser/oracle_ir/ast.rs
  • crates/engine/src/parser/oracle_ir/doc.rs
  • crates/engine/src/parser/oracle_trigger.rs
  • crates/engine/src/types/ability.rs
  • crates/engine/tests/integration/issue_6504_jeweled_amulet_noted_mana.rs
  • crates/engine/tests/integration/main.rs
  • crates/engine/tests/integration/oracle_parser.rs
  • crates/mtgish-import/src/convert/action.rs
  • crates/phase-ai/src/features/devotion.rs
  • crates/phase-ai/src/mana_colors.rs
  • crates/phase-ai/src/policies/effect_classify.rs
  • crates/phase-ai/src/policies/redundancy_avoidance.rs

Comment thread crates/engine/src/game/ability_scan.rs Outdated
Comment thread crates/engine/src/game/casting.rs Outdated
Comment thread crates/engine/src/game/effects/mana.rs

@matthewevans matthewevans left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Changes requested — supplemental current-head blockers.

🔴 Blocker

game/ability_scan.rs:4959-4968 treats ManaProduction::NotedType as count-only and never asserts sibling: true, but the producer reads mutable per-source chosen_attributes through effects/mana.rs:856-865. The scanner’s own axis contract at ability_scan.rs:109-116 requires sibling-sensitive state to participate in loop-firewall classification. Add the required sibling axis/coverage so sibling activations cannot be treated as independent when their noted-mana state is shared/mutable.

🟡 Required tests and gate

issue_6504_jeweled_amulet_noted_mana.rs:77 immediately resolves the activation, so it cannot prove the note is absent before resolution; :127-155 uses a new amulet, so it cannot detect overwrite/append behavior on one object. Add reachability tests for both semantics alongside the already requested bounce/flicker case. The current Rust lint failure is the parser-combinator gate rejecting the new string dispatch at parser/oracle_effect/imperative.rs:10128; rewrite it with the repository’s required nom dispatch convention.

The current parse-diff is now present and confirms the earlier semantic concern: it adds NoteManaSpent for Ice Cauldron while adding Mana(NotedType) only for Jeweled Amulet. The earlier type-and-amount and source-incarnation blockers remain unchanged.

Recommendation: address these supplemental authority/test/gate defects together with the existing current-head blockers, then request re-review.

jsdevninja added a commit to jsdevninja/phase that referenced this pull request Jul 30, 2026
…n staleness

Two review findings on PR phase-rs#6812 (issue phase-rs#6504):

- The parser accepted Ice Cauldron's "note the type AND AMOUNT of mana
  spent..." wording and routed it through the same Effect::NoteManaSpent
  as Jeweled Amulet's singular-type wording, but the resolver only ever
  stores/reproduces the first noted type — it doesn't model Ice
  Cauldron's exact stored multiset or its spend restriction. Narrowed
  the parser (and every doc comment) to Jeweled Amulet's exact
  "note the type of mana spent..." wording only; Ice Cauldron's text is
  now intentionally left unmatched and still reports Unimplemented.
  This also fixes a parser-combinator-gate violation: the narrowed
  recognizer moved out of the `match first_word { "note" => .. }`
  string-literal dispatch (flagged as new bare-string-match-arm code)
  into an anchored nom `all_consuming` guard, mirroring the existing
  "end the turn" precedent in the same function.

- `Effect::NoteManaSpent` read/wrote its source purely via `ability.
  source_id`, with no check that the object at that storage id was
  still the same incarnation that made the payment (CR 400.7). A
  source bounced or flickered while its OWN "note" ability sat
  unresolved on the stack would have the OLD incarnation's payment
  silently promoted onto the NEW incarnation's `chosen_attributes`.
  `GameObject::mana_spent_to_activate` is now paired with the
  incarnation captured at payment time
  (`mana_spent_to_activate_incarnation`); the resolver refuses to
  write unless the object's live incarnation still matches, mirroring
  the engine's existing incarnation-pairing idiom used by
  `ResolvedAbility::source_is_current` / `TargetFilter::SelfRef`
  resolution. Added a stack-level bounce regression test, verified
  revert-failing.
@jsdevninja

Copy link
Copy Markdown
Contributor Author

Addressed both blockers:

Ice Cauldron overreach — narrowed the parser to Jeweled Amulet's exact singular-type wording only ("note the type of mana spent to pay this activation cost"). Ice Cauldron's "note the type AND AMOUNT..." is now intentionally left unmatched and still reports Effect::Unimplemented, unchanged from before this PR. This also happened to fix a check-parser-combinators.sh gate violation — the recognizer now lives outside the match first_word { "note" => .. } string-literal dispatch, as an anchored nom all_consuming guard (mirrors the existing "end the turn" precedent in the same function), so no new bare-string-match-arm code was added.

Source incarnationGameObject::mana_spent_to_activate is now paired with mana_spent_to_activate_incarnation, captured at the same payment site. Effect::NoteManaSpent refuses to promote the latch to durable state unless the object's live incarnation still matches, mirroring the engine's existing incarnation-pairing idiom (ResolvedAbility::source_is_current / TargetFilter::SelfRef). Added jeweled_amulet_bounced_mid_stack_does_not_note_on_new_incarnation, which bounces the amulet through the real bounce::resolve resolver while its own note ability sits unresolved on the stack, then lets the stack resolve and asserts the new incarnation notes nothing — verified this fails without the fix (temporarily reverted the guard locally, confirmed the test fails, restored it).

The CI lint-gate failure was specifically the parser-combinator-gate violation above; re-ran ./scripts/check-parser-combinators.sh locally against latest main and it passes. Full local verification (fmt, clippy -D warnings, parser gate, cargo test -p phase-engine at 18k+ lib / 4.2k+ integration tests, cargo test -p phase-ai) is green on the pushed commit.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@crates/engine/src/game/casting.rs`:
- Around line 14725-14744: Bind each activation’s mana-payment snapshot to its
individual pending activation or stack entry, including the source incarnation,
instead of storing it only in GameObject::mana_spent_to_activate. Update the
payment flow around the shown source-mutation logic to attach the snapshot to
the activation record, and update NoteManaSpent resolution to consume that exact
record while rejecting incarnation mismatches; preserve support for both
direct-activation and PendingCast paths without relying on tap-cost behavior.

In `@crates/engine/tests/integration/issue_6504_jeweled_amulet_noted_mana.rs`:
- Around line 211-280: Prove ability 0’s payment was actually recorded before
the bounce by querying P0’s mana pool immediately after ActivateAbility and
asserting the single red mana unit was drained. Add this reach-guard before
capturing incarnation_before or invoking bounce::resolve, using the existing
mana-pool query helper in the test file; preserve the current bounce and final
noted_mana_spent assertions.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 466b644a-a929-4edb-b725-be04df019bd9

📥 Commits

Reviewing files that changed from the base of the PR and between a767997 and 65625bc.

📒 Files selected for processing (7)
  • crates/engine/src/game/casting.rs
  • crates/engine/src/game/effects/note_mana_spent.rs
  • crates/engine/src/game/game_object.rs
  • crates/engine/src/parser/oracle_effect/imperative.rs
  • crates/engine/src/parser/oracle_ir/ast.rs
  • crates/engine/src/types/ability.rs
  • crates/engine/tests/integration/issue_6504_jeweled_amulet_noted_mana.rs
🚧 Files skipped from review as they are similar to previous changes (1)
  • crates/engine/src/types/ability.rs

Comment thread crates/engine/src/game/casting.rs Outdated

@matthewevans matthewevans left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Changes requested — current-head paid-mana attribution remains unsafe.

🔴 Blocker

game/casting.rs:14725-14743 writes every activation’s paid mana onto the mutable source-global GameObject::mana_spent_to_activate, explicitly overwriting it on the next activation. effects/note_mana_spent.rs:42-52 later reads that mutable latch and verifies only the object’s current incarnation, not the resolving ability’s own payment. If an unresolved Jeweled Amulet activation is untapped and activated again with a different color, both note effects can read the later payment. Capture the spent mana and source incarnation in the activated stack/ResolvedAbility context, then consume the resolving ability’s snapshot; add a LIFO stacked-activation regression.

🔴 Blocker

game/ability_scan.rs:4959-4968 still treats ManaProduction::NotedType as count-only, even though effects/mana.rs:840-853 reads mutable per-source ChosenAttribute::NotedManaSpent. The scan contract at ability_scan.rs:109-116 classifies that as sibling-mutable state. Assert sibling: true for this producer and add the corresponding loop-firewall regression.

🟡 Required evidence

issue_6504_jeweled_amulet_noted_mana.rs:67-84 immediately resolves the helper activation, so it cannot prove no durable note before resolution/countering; its red and green checks use separate objects (:90-151), so they do not establish replace-versus-append behavior. Add those discriminating cases. The only parse-diff sticky predates this head and still lists Ice Cauldron as supported; regenerate and reconcile current-head evidence for the intentional narrowing.

Recommendation: retain the good narrowing and bounce fix, but make paid-mana provenance per activation, correct the scan axis, add the missing tests, and regenerate parse evidence before re-review.

jsdevninja added a commit to jsdevninja/phase that referenced this pull request Jul 30, 2026
Second round of review on PR phase-rs#6812 (issue phase-rs#6504):

- game/casting.rs / game/casting_costs.rs / effects/note_mana_spent.rs:
  GameObject::mana_spent_to_activate was a per-source mutable latch that
  the NEXT activation's cost payment silently overwrote. Since the
  incarnation guard added in the prior round only protects against a
  bounced/flickered SOURCE, not a same-incarnation permanent untapped and
  reactivated while an earlier "note" activation still sits unresolved
  on the stack (legal: "Activate only if no charge counters" is checked
  at activation, and no counter exists yet while the first note ability
  is still unresolved), two stacked activations could both observe the
  LATER payment. `push_ability_entry` — the single authority where an
  activated ability reaches the stack — now drains that latch
  synchronously into THIS activation's own `ResolvedAbility::
  noted_mana_payment` snapshot (a new `NotedManaPayment { types,
  source_incarnation }`, propagated through `sub_ability`/`else_ability`
  via a new `set_noted_mana_payment_recursive`, since `Effect::
  NoteManaSpent` is chained as a sub-ability with its own separate
  ResolvedAbility node) immediately after cost payment completes, before
  any later activation of the same permanent can occur.
  `Effect::NoteManaSpent` now reads that per-activation snapshot instead
  of the shared object field. Added a LIFO stacked-activation regression
  (`jeweled_amulet_lifo_stacked_activations_each_note_their_own_payment`)
  that activates twice with different colors before either resolves and
  asserts each resolution notes its own payment.

- game/ability_scan.rs: `ManaProduction::NotedType` read
  `ChosenAttribute::NotedManaSpent`, a per-object value a SIBLING copy of
  `Effect::NoteManaSpent` can mutate before this production resolves —
  exactly the race above — so it must self-assert `sibling: true` (CR
  603.3b ordering-relevance) rather than sit in the count-only bucket
  alongside `ChosenColor` (whose as-enters choice is effectively fixed
  for the object's lifetime and has no such sibling-mutation risk). Added
  `noted_mana_type_self_asserts_sibling`.

- issue_6504_jeweled_amulet_noted_mana.rs: added
  `jeweled_amulet_notes_nothing_before_resolution` (CR 608.2c: no durable
  note before the ability actually resolves) and
  `jeweled_amulet_second_note_replaces_not_appends` (two full note
  cycles on the SAME object leave exactly one entry, not two) — the
  prior round's red/green tests used separate objects and an
  always-resolved helper, so neither claim was actually exercised.

`noted_mana_payment` is a new `ResolvedAbility` field; every exhaustive
match/destructure/literal across the crate (batch-candidate proofs in
stack.rs, inert-trigger equality, several no-target `ResolvedAbility`
literals in effects/*.rs) is updated in lockstep, including two new
"must not batch a per-activation payment snapshot" gates mirroring the
existing `cost_paid_object` ones.
@jsdevninja

Copy link
Copy Markdown
Contributor Author

Addressed both blockers:

Per-activation paid-mana attributionGameObject::mana_spent_to_activate was a per-source mutable latch the NEXT activation's payment silently overwrote; the incarnation guard from the prior round only protects against a bounced/flickered source, not a same-incarnation permanent untapped and reactivated while an earlier note activation still sits unresolved (legal: "Activate only if no charge counters" is checked at activation, and no counter exists yet while the first note ability is unresolved). push_ability_entry — the single authority where an activated ability reaches the stack — now drains that latch synchronously into a new ResolvedAbility::noted_mana_payment snapshot (NotedManaPayment { types, source_incarnation }) immediately after cost payment completes, before any later activation of the same permanent can occur. Effect::NoteManaSpent reads that per-activation snapshot instead of the shared object field.

One wrinkle this surfaced mid-fix: Effect::NoteManaSpent is chained as a sub_ability (PutCounter { sub_ability: NoteManaSpent }), which resolves as its own separate ResolvedAbility node distinct from the top-level ability the payment gets captured onto. A flat assignment left the sub-ability reading the field's None default (my own regression tests caught this immediately — two previously-passing tests started failing). Added ResolvedAbility::set_noted_mana_payment_recursive (mirrors the existing set_chosen_x_recursive/set_cost_paid_object_recursive pattern) to propagate it through sub_ability/else_ability.

Added jeweled_amulet_lifo_stacked_activations_each_note_their_own_payment: activates twice with different colors before either resolves (untapping the amulet in between to simulate an external untap effect), resolves LIFO, and asserts each resolution notes its own payment, not the sibling's. Verified this fails on the pre-fix shared-latch version.

Sibling classificationManaProduction::NotedType reads ChosenAttribute::NotedManaSpent, a per-object value a sibling Effect::NoteManaSpent can mutate before this production resolves — exactly the race above — so it now self-asserts sibling: true rather than sitting in the count-only bucket alongside ChosenColor (whose as-enters choice is fixed for the object's lifetime and has no sibling-mutation risk). Added noted_mana_type_self_asserts_sibling.

Test evidence — added jeweled_amulet_notes_nothing_before_resolution (CR 608.2c: no durable note before actual resolution) and jeweled_amulet_second_note_replaces_not_appends (two full note cycles on the same object leave exactly one entry). The prior round's red/green tests used separate objects and an always-resolved helper, so neither claim was actually exercised before.

noted_mana_payment is a new ResolvedAbility field; updated every exhaustive match/destructure/literal it touched across the crate (batch-candidate proofs in stack.rs, inert-trigger equality, several no-target ResolvedAbility literals in effects/*.rs), including two new "must not batch a per-activation payment snapshot" gates mirroring the existing cost_paid_object ones.

Local verification on the pushed commit: cargo fmt --all, cargo clippy --all-targets --workspace -- -D warnings, ./scripts/check-parser-combinators.sh, cargo test -p phase-engine (18k+ lib / 4.2k+ integration tests), cargo test -p phase-ai all green.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
crates/engine/src/types/ability.rs (1)

1471-1483: 📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Stale doc: "transient mana_spent_to_activate latch" contradicts the per-activation snapshot design.

Both ChosenAttribute::NotedManaSpent's doc and Effect::NoteManaSpent's doc describe the noted-mana data as read from "the source's transient mana_spent_to_activate payment latch" at resolution time. But ResolvedAbility::noted_mana_payment (and its accompanying doc, plus the commit history: "refined the implementation from per-object activation payment storage to per-activation snapshots") makes clear the actual mechanism captures payment once per activation on the ResolvedAbility at stack-push time — specifically to prevent stacked activations of the same permanent from clobbering each other's payment before either resolves.

As written, these two comments describe the exact bug ("a per-object mutable latch") that the per-activation snapshot was introduced to fix, which will mislead anyone maintaining this code later. Please update both doc comments to say Effect::NoteManaSpent reads ability.noted_mana_payment (the per-activation snapshot stamped by push_ability_entry/set_noted_mana_payment_recursive), not a live per-object latch.

Also applies to: 12589-12603

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@crates/engine/src/types/ability.rs` around lines 1471 - 1483, Update the
documentation for ChosenAttribute::NotedManaSpent and Effect::NoteManaSpent to
describe reading ability.noted_mana_payment, the per-activation snapshot stamped
by push_ability_entry/set_noted_mana_payment_recursive. Remove references to the
source's transient mana_spent_to_activate latch while preserving the existing
zone-clearing and replacement behavior documentation.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Outside diff comments:
In `@crates/engine/src/types/ability.rs`:
- Around line 1471-1483: Update the documentation for
ChosenAttribute::NotedManaSpent and Effect::NoteManaSpent to describe reading
ability.noted_mana_payment, the per-activation snapshot stamped by
push_ability_entry/set_noted_mana_payment_recursive. Remove references to the
source's transient mana_spent_to_activate latch while preserving the existing
zone-clearing and replacement behavior documentation.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: aad4c36a-5f42-4ffd-ad9b-52caa869e72c

📥 Commits

Reviewing files that changed from the base of the PR and between 65625bc and 7e9c7ba.

📒 Files selected for processing (19)
  • crates/engine/src/game/ability_rw.rs
  • crates/engine/src/game/ability_scan.rs
  • crates/engine/src/game/casting.rs
  • crates/engine/src/game/casting_costs.rs
  • crates/engine/src/game/effects/additional_phase.rs
  • crates/engine/src/game/effects/double.rs
  • crates/engine/src/game/effects/extra_turn.rs
  • crates/engine/src/game/effects/grant_extra_loyalty_activations.rs
  • crates/engine/src/game/effects/note_mana_spent.rs
  • crates/engine/src/game/effects/player_counter.rs
  • crates/engine/src/game/effects/reverse_turn_order.rs
  • crates/engine/src/game/effects/skip_next_step.rs
  • crates/engine/src/game/effects/skip_next_turn.rs
  • crates/engine/src/game/effects/vote.rs
  • crates/engine/src/game/game_object.rs
  • crates/engine/src/game/stack.rs
  • crates/engine/src/types/ability.rs
  • crates/engine/tests/integration/issue_6504_jeweled_amulet_noted_mana.rs
  • crates/engine/tests/integration/the_chain_veil_loyalty_grants.rs
🚧 Files skipped from review as they are similar to previous changes (1)
  • crates/engine/src/game/ability_rw.rs

@matthewevans matthewevans self-assigned this Jul 30, 2026
@matthewevans matthewevans added the bug Bug fix label Jul 30, 2026

@matthewevans matthewevans left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔴 Blocker — ability copies retain an activation-only payment snapshot

copy_spell.rs:109-130 clones the entire StackEntryKind, then the activated-ability arm only preserves the original source. That leaves ResolvedAbility::noted_mana_payment (including nested sub_ability/else_ability nodes) intact. note_mana_spent.rs:46-70 consumes that snapshot when the copied ability resolves, so a copied Jeweled Amulet activation can write the original activation's paid mana colors even though the copy did not pay an activation cost.

CR 707.10 says a copied activated ability is not activated. The original's announced decisions and objects used to pay costs are copyable where relevant, but this engine's noted_mana_payment is an activation-payment observation for NoteManaSpent, not a decision or cost object that the copy paid. Preserving it produces a false durable note on resolution.

Please clear noted_mana_payment recursively when copying activated abilities (while retaining the existing CR 707.10b source preservation), and add a runtime regression that copies an activated Jeweled Amulet ability after a colored original payment and proves the copied ability cannot note those original colors. The regression should exercise the copy stack path and resolution rather than only inspecting a constructor.

The existing source-incarnation and per-activation provenance fixes are valuable, but they do not distinguish an original activation from a copy of that activation.

@matthewevans matthewevans removed their assignment Jul 30, 2026
jsdevninja added a commit to jsdevninja/phase that referenced this pull request Jul 30, 2026
Third round of review on PR phase-rs#6812 (issue phase-rs#6504):

copy_spell.rs's ActivatedAbility/TriggeredAbility copy arm only called
preserve_ability_copy_source_recursive to re-stamp source_id; the rest
of the cloned ResolvedAbility chain, including the new
noted_mana_payment field, rode along unchanged. CR 707.10: a copy of an
activated ability is not itself activated, so it never paid a mana
cost — but a copied Jeweled Amulet activation still carried the
ORIGINAL's captured payment, and Effect::NoteManaSpent resolving on
the copy would falsely note colors the copy never spent.

preserve_ability_copy_source_recursive now also calls a new
ResolvedAbility::clear_noted_mana_payment_recursive (mirrors
set_noted_mana_payment_recursive's recursion shape in reverse) so the
copy's whole chain — including any sub_ability/else_ability nodes —
loses the inherited snapshot.

Added jeweled_amulet_copied_activation_does_not_note_original_payment,
which exercises the real copy pipeline (copy_spell::resolve targeting
the original activation's own stack-entry id, the same stack-entry
lookup and LIFO resolution a real "copy target activated or triggered
ability" card like Lithoform Engine drives) rather than only unit
-testing the clearing method: activates paying red, copies it,
resolves the copy first (asserting no note) and the original second
(asserting it still notes red — the paired positive reach-guard proving
the fix clears only the copy's snapshot). Verified revert-failing.
@jsdevninja

Copy link
Copy Markdown
Contributor Author

Addressed: CR 707.10 — copies of Jeweled Amulet's activation don't pay a cost, so they must not inherit the original's noted-mana payment.

copy_spell.rs's ActivatedAbility/TriggeredAbility copy arm only re-stamped source_id via preserve_ability_copy_source_recursive; the rest of the cloned ResolvedAbility chain — including noted_mana_payment — rode along unchanged from the struct clone. Added ResolvedAbility::clear_noted_mana_payment_recursive (mirrors set_noted_mana_payment_recursive's recursion shape in reverse) and call it from preserve_ability_copy_source_recursive, so the copy's whole chain (including any sub_ability/else_ability nodes) loses the inherited snapshot.

Added jeweled_amulet_copied_activation_does_not_note_original_payment, which exercises the real copy pipeline rather than only unit-testing the clearing method: activates paying red, then calls copy_spell::resolve directly targeting the original activation's own stack-entry id (the same stack-entry lookup and LIFO resolution a real "copy target activated or triggered ability" card like Lithoform Engine drives through the full pipeline — I found and mirrored the existing copied_ability_transform_generation.rs test for that mechanism). Resolves the copy first and asserts it notes nothing (even though both copies' PutCounter still fire unconditionally — CR 602.5/608.2c: the "no charge counters" restriction gates activation, never re-checked at resolution, so counter count alone can't distinguish correct from buggy behavior here), then resolves the original second and asserts it still notes red — the paired positive reach-guard proving the fix clears only the copy's snapshot, not the original's. Verified revert-failing (temporarily removed the clear call, confirmed the test fails, restored it).

Local verification on the pushed commit: cargo fmt --all, cargo clippy --all-targets --workspace -- -D warnings, ./scripts/check-parser-combinators.sh, cargo test -p phase-engine (18k+ lib / 4.2k+ integration tests) all green.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@crates/engine/tests/integration/issue_6504_jeweled_amulet_noted_mana.rs`:
- Around line 487-595: Add an assertion immediately before the
copy_spell::resolve call that the original stack entry identified by
original_entry_id still carries its noted_mana_payment snapshot. Use the stack
entry’s resolved ability data and assert the snapshot is present, so the later
is_none() check specifically verifies that NoteManaSpent on the copied
activation does not inherit it.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 3b7b506e-a8fd-4945-ad77-beedadd02ed5

📥 Commits

Reviewing files that changed from the base of the PR and between 7e9c7ba and 3f152bc.

📒 Files selected for processing (3)
  • crates/engine/src/game/effects/copy_spell.rs
  • crates/engine/src/types/ability.rs
  • crates/engine/tests/integration/issue_6504_jeweled_amulet_noted_mana.rs

@matthewevans matthewevans self-assigned this Jul 30, 2026

@matthewevans matthewevans left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Changes requested — the new effect is still reached through a verbatim, one-card Oracle-text match.

🔴 Blocker

crates/engine/src/parser/oracle_effect/imperative.rs:9765-9782 recognizes the entire normalized sentence "note the type of mana spent to pay this activation cost" in one all_consuming(terminated(tag(...))) branch. The confirmed Jeweled Amulet Oracle text is "Note the type of mana spent to pay this activation cost."; the current parser therefore hard-codes that card sentence rather than parsing the grammatical "note [the] type of mana spent to pay this [activation] cost" structure. This violates the parser's composable-building-block boundary and leaves even straightforward grammatical siblings as unrelated future special cases.

Please move this into a named nom grammar parser that composes the instruction prefix, the noted-mana subject, and the activation-cost referent (with explicit, typed boundaries), then add building-block tests for the accepted structural variants and a negative boundary. Keep Ice Cauldron unsupported unless its distinct amount-and-spend-restriction semantics are modeled end-to-end.

✅ Clean

The current head's per-activation ResolvedAbility::noted_mana_payment snapshot and recursive clearing on copied ability chains address the prior aliasing and copied-ability findings. The copy regression's final original-resolution assertion is also a sufficient positive reach guard for the original snapshot: it would fail if the clone cleanup erased or never preserved it.

Recommendation: request changes for the parser seam, then re-run the current-head parser and runtime evidence.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@crates/engine/src/parser/oracle_effect/imperative.rs`:
- Around line 13511-13560: The tests only exercise parse_note_mana_spent_clause,
so add end-to-end production parser assertions for both cards. Verify Jeweled
Amulet’s complete wording lowers to Effect::NoteManaSpent, and verify Ice
Cauldron’s type-and-amount wording reaches Effect::Unimplemented; include the
positive reach guard before the negative assertion as required by the parser
test conventions.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 171c54de-2992-4d65-9425-9edbab4c1c01

📥 Commits

Reviewing files that changed from the base of the PR and between 3f152bc and 0f6bfbf.

📒 Files selected for processing (2)
  • crates/engine/src/parser/oracle_effect/imperative.rs
  • crates/engine/tests/integration/oracle_parser.rs

Comment thread crates/engine/src/parser/oracle_effect/imperative.rs Outdated
@matthewevans

Copy link
Copy Markdown
Member

Maintainer hold — current head 11112a640a69bc68b9b9cbec8143ef22f3b348cb.

I pushed the narrow documentation correction: both relevant ability.rs comments now identify the resolving activation’s ResolvedAbility::noted_mana_payment snapshot; mana_spent_to_activate is only the activation-time bridge into that snapshot.

No approval or enqueue yet. The current-head CI run is still queued/in progress (AI contributor labels, paired-seed AI gate, decision-cost perf gate, Superagent Security Scan, and CodeRabbit), and the only <!-- coverage-parse-diff --> artifact was updated for the previous head 0f6bfbf23e59e5c7d09b0211281b65a6d52fcff7.

Next step: wait for those checks to settle and for a parse-diff artifact generated from this head, then resume the final review/enqueue decision.

@matthewevans

Copy link
Copy Markdown
Member

Current-head maintainer hold — performance evidence is terminal red.

Head 11112a640a69bc68b9b9cbec8143ef22f3b348cb has a failing Decision-cost perf gate: restriction_static_mode_gate_scans is 59,693, above its 50,476 threshold (baseline 48,012), and the report also contains the new spell_keyword_grant_scans counter. The gate explicitly requires a performance-schema/baseline refresh for the changed counter set, but the threshold overrun must first be explained or reduced. This is not a maintainer-fixup-sized repair, so I have not pushed a speculative baseline/schema update, approved, or enabled auto-merge. Please provide a current-head perf diagnosis and a focused correction or justified refreshed baseline; review/enqueue resumes after the required gate is green.

@matthewevans matthewevans removed their assignment Jul 31, 2026

@matthewevans matthewevans left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔴 Blocker — current head does not compile

Reviewed commit 56fc5b9e8c18e608b4a12da20966c386df226b66.

Current merge CI is terminal red: Rust lint, Rust tests shard 1, Rust tests shard 2, WASM, Card data, paired-seed AI, and decision-cost perf.

The terminal Rust compiler failures require explicit handling of the new data:

  1. ResolvedAbility::noted_mana_payment is omitted from the explicit destructures in crates/engine/src/game/ability_scan.rs:209 and crates/engine/src/game/resolution_prompt.rs:523.
  2. Effect::NoteManaSpent is omitted from the exhaustive effect classifications in crates/engine/src/game/ability_scan.rs:408, :5218, and :5933; crates/engine/src/game/resolution_prompt.rs:210; and crates/engine/src/types/ability_visit.rs:276.

Please classify both additions deliberately at every affected seam—whether each is carried, scanned, visited, or intentionally ignored. Do not resolve this with wildcard match arms or destructure ..; these sites represent distinct semantic classifications and must stay explicit. Add or update focused tests for those classifications.

Afterward, provide fresh green CI and a current-head parse-diff. The existing parse-diff predates this head, so it is not evidence for 56fc5b9e8c18e608b4a12da20966c386df226b66.

matthewevans pushed a commit to jsdevninja/phase that referenced this pull request Aug 7, 2026
…n staleness

Two review findings on PR phase-rs#6812 (issue phase-rs#6504):

- The parser accepted Ice Cauldron's "note the type AND AMOUNT of mana
  spent..." wording and routed it through the same Effect::NoteManaSpent
  as Jeweled Amulet's singular-type wording, but the resolver only ever
  stores/reproduces the first noted type — it doesn't model Ice
  Cauldron's exact stored multiset or its spend restriction. Narrowed
  the parser (and every doc comment) to Jeweled Amulet's exact
  "note the type of mana spent..." wording only; Ice Cauldron's text is
  now intentionally left unmatched and still reports Unimplemented.
  This also fixes a parser-combinator-gate violation: the narrowed
  recognizer moved out of the `match first_word { "note" => .. }`
  string-literal dispatch (flagged as new bare-string-match-arm code)
  into an anchored nom `all_consuming` guard, mirroring the existing
  "end the turn" precedent in the same function.

- `Effect::NoteManaSpent` read/wrote its source purely via `ability.
  source_id`, with no check that the object at that storage id was
  still the same incarnation that made the payment (CR 400.7). A
  source bounced or flickered while its OWN "note" ability sat
  unresolved on the stack would have the OLD incarnation's payment
  silently promoted onto the NEW incarnation's `chosen_attributes`.
  `GameObject::mana_spent_to_activate` is now paired with the
  incarnation captured at payment time
  (`mana_spent_to_activate_incarnation`); the resolver refuses to
  write unless the object's live incarnation still matches, mirroring
  the engine's existing incarnation-pairing idiom used by
  `ResolvedAbility::source_is_current` / `TargetFilter::SelfRef`
  resolution. Added a stack-level bounce regression test, verified
  revert-failing.
matthewevans pushed a commit to jsdevninja/phase that referenced this pull request Aug 7, 2026
Second round of review on PR phase-rs#6812 (issue phase-rs#6504):

- game/casting.rs / game/casting_costs.rs / effects/note_mana_spent.rs:
  GameObject::mana_spent_to_activate was a per-source mutable latch that
  the NEXT activation's cost payment silently overwrote. Since the
  incarnation guard added in the prior round only protects against a
  bounced/flickered SOURCE, not a same-incarnation permanent untapped and
  reactivated while an earlier "note" activation still sits unresolved
  on the stack (legal: "Activate only if no charge counters" is checked
  at activation, and no counter exists yet while the first note ability
  is still unresolved), two stacked activations could both observe the
  LATER payment. `push_ability_entry` — the single authority where an
  activated ability reaches the stack — now drains that latch
  synchronously into THIS activation's own `ResolvedAbility::
  noted_mana_payment` snapshot (a new `NotedManaPayment { types,
  source_incarnation }`, propagated through `sub_ability`/`else_ability`
  via a new `set_noted_mana_payment_recursive`, since `Effect::
  NoteManaSpent` is chained as a sub-ability with its own separate
  ResolvedAbility node) immediately after cost payment completes, before
  any later activation of the same permanent can occur.
  `Effect::NoteManaSpent` now reads that per-activation snapshot instead
  of the shared object field. Added a LIFO stacked-activation regression
  (`jeweled_amulet_lifo_stacked_activations_each_note_their_own_payment`)
  that activates twice with different colors before either resolves and
  asserts each resolution notes its own payment.

- game/ability_scan.rs: `ManaProduction::NotedType` read
  `ChosenAttribute::NotedManaSpent`, a per-object value a SIBLING copy of
  `Effect::NoteManaSpent` can mutate before this production resolves —
  exactly the race above — so it must self-assert `sibling: true` (CR
  603.3b ordering-relevance) rather than sit in the count-only bucket
  alongside `ChosenColor` (whose as-enters choice is effectively fixed
  for the object's lifetime and has no such sibling-mutation risk). Added
  `noted_mana_type_self_asserts_sibling`.

- issue_6504_jeweled_amulet_noted_mana.rs: added
  `jeweled_amulet_notes_nothing_before_resolution` (CR 608.2c: no durable
  note before the ability actually resolves) and
  `jeweled_amulet_second_note_replaces_not_appends` (two full note
  cycles on the SAME object leave exactly one entry, not two) — the
  prior round's red/green tests used separate objects and an
  always-resolved helper, so neither claim was actually exercised.

`noted_mana_payment` is a new `ResolvedAbility` field; every exhaustive
match/destructure/literal across the crate (batch-candidate proofs in
stack.rs, inert-trigger equality, several no-target `ResolvedAbility`
literals in effects/*.rs) is updated in lockstep, including two new
"must not batch a per-activation payment snapshot" gates mirroring the
existing `cost_paid_object` ones.
matthewevans pushed a commit to jsdevninja/phase that referenced this pull request Aug 7, 2026
Third round of review on PR phase-rs#6812 (issue phase-rs#6504):

copy_spell.rs's ActivatedAbility/TriggeredAbility copy arm only called
preserve_ability_copy_source_recursive to re-stamp source_id; the rest
of the cloned ResolvedAbility chain, including the new
noted_mana_payment field, rode along unchanged. CR 707.10: a copy of an
activated ability is not itself activated, so it never paid a mana
cost — but a copied Jeweled Amulet activation still carried the
ORIGINAL's captured payment, and Effect::NoteManaSpent resolving on
the copy would falsely note colors the copy never spent.

preserve_ability_copy_source_recursive now also calls a new
ResolvedAbility::clear_noted_mana_payment_recursive (mirrors
set_noted_mana_payment_recursive's recursion shape in reverse) so the
copy's whole chain — including any sub_ability/else_ability nodes —
loses the inherited snapshot.

Added jeweled_amulet_copied_activation_does_not_note_original_payment,
which exercises the real copy pipeline (copy_spell::resolve targeting
the original activation's own stack-entry id, the same stack-entry
lookup and LIFO resolution a real "copy target activated or triggered
ability" card like Lithoform Engine drives) rather than only unit
-testing the clearing method: activates paying red, copies it,
resolves the copy first (asserting no note) and the original second
(asserting it still notes red — the paired positive reach-guard proving
the fix clears only the copy's snapshot). Verified revert-failing.
@matthewevans
matthewevans force-pushed the fix/6504-jeweled-amulet-noted-mana branch 2 times, most recently from 8dd5053 to 0fc5c36 Compare August 7, 2026 21:44
jsdevninja and others added 8 commits August 7, 2026 15:05
…se-rs#6504)

Jeweled Amulet's first ability placed a charge counter but never noted
the mana type spent to pay its own activation cost, so the second
ability ("Add one mana of this artifact's last noted type") always
produced zero mana. Both clauses fell through to Effect::Unimplemented.

Adds the noted-mana-type building block shared by this small card class
(Jeweled Amulet, Ice Cauldron): a transient GameObject payment latch
stamped at ability-mana-cost payment, a ChosenAttribute::NotedManaSpent
slot written by a new Effect::NoteManaSpent at resolution (so a
countered ability never notes anything), and a ManaProduction::NotedType
variant that reads it back at CR 106.5-correct empty-set behavior.
…n staleness

Two review findings on PR phase-rs#6812 (issue phase-rs#6504):

- The parser accepted Ice Cauldron's "note the type AND AMOUNT of mana
  spent..." wording and routed it through the same Effect::NoteManaSpent
  as Jeweled Amulet's singular-type wording, but the resolver only ever
  stores/reproduces the first noted type — it doesn't model Ice
  Cauldron's exact stored multiset or its spend restriction. Narrowed
  the parser (and every doc comment) to Jeweled Amulet's exact
  "note the type of mana spent..." wording only; Ice Cauldron's text is
  now intentionally left unmatched and still reports Unimplemented.
  This also fixes a parser-combinator-gate violation: the narrowed
  recognizer moved out of the `match first_word { "note" => .. }`
  string-literal dispatch (flagged as new bare-string-match-arm code)
  into an anchored nom `all_consuming` guard, mirroring the existing
  "end the turn" precedent in the same function.

- `Effect::NoteManaSpent` read/wrote its source purely via `ability.
  source_id`, with no check that the object at that storage id was
  still the same incarnation that made the payment (CR 400.7). A
  source bounced or flickered while its OWN "note" ability sat
  unresolved on the stack would have the OLD incarnation's payment
  silently promoted onto the NEW incarnation's `chosen_attributes`.
  `GameObject::mana_spent_to_activate` is now paired with the
  incarnation captured at payment time
  (`mana_spent_to_activate_incarnation`); the resolver refuses to
  write unless the object's live incarnation still matches, mirroring
  the engine's existing incarnation-pairing idiom used by
  `ResolvedAbility::source_is_current` / `TargetFilter::SelfRef`
  resolution. Added a stack-level bounce regression test, verified
  revert-failing.
Second round of review on PR phase-rs#6812 (issue phase-rs#6504):

- game/casting.rs / game/casting_costs.rs / effects/note_mana_spent.rs:
  GameObject::mana_spent_to_activate was a per-source mutable latch that
  the NEXT activation's cost payment silently overwrote. Since the
  incarnation guard added in the prior round only protects against a
  bounced/flickered SOURCE, not a same-incarnation permanent untapped and
  reactivated while an earlier "note" activation still sits unresolved
  on the stack (legal: "Activate only if no charge counters" is checked
  at activation, and no counter exists yet while the first note ability
  is still unresolved), two stacked activations could both observe the
  LATER payment. `push_ability_entry` — the single authority where an
  activated ability reaches the stack — now drains that latch
  synchronously into THIS activation's own `ResolvedAbility::
  noted_mana_payment` snapshot (a new `NotedManaPayment { types,
  source_incarnation }`, propagated through `sub_ability`/`else_ability`
  via a new `set_noted_mana_payment_recursive`, since `Effect::
  NoteManaSpent` is chained as a sub-ability with its own separate
  ResolvedAbility node) immediately after cost payment completes, before
  any later activation of the same permanent can occur.
  `Effect::NoteManaSpent` now reads that per-activation snapshot instead
  of the shared object field. Added a LIFO stacked-activation regression
  (`jeweled_amulet_lifo_stacked_activations_each_note_their_own_payment`)
  that activates twice with different colors before either resolves and
  asserts each resolution notes its own payment.

- game/ability_scan.rs: `ManaProduction::NotedType` read
  `ChosenAttribute::NotedManaSpent`, a per-object value a SIBLING copy of
  `Effect::NoteManaSpent` can mutate before this production resolves —
  exactly the race above — so it must self-assert `sibling: true` (CR
  603.3b ordering-relevance) rather than sit in the count-only bucket
  alongside `ChosenColor` (whose as-enters choice is effectively fixed
  for the object's lifetime and has no such sibling-mutation risk). Added
  `noted_mana_type_self_asserts_sibling`.

- issue_6504_jeweled_amulet_noted_mana.rs: added
  `jeweled_amulet_notes_nothing_before_resolution` (CR 608.2c: no durable
  note before the ability actually resolves) and
  `jeweled_amulet_second_note_replaces_not_appends` (two full note
  cycles on the SAME object leave exactly one entry, not two) — the
  prior round's red/green tests used separate objects and an
  always-resolved helper, so neither claim was actually exercised.

`noted_mana_payment` is a new `ResolvedAbility` field; every exhaustive
match/destructure/literal across the crate (batch-candidate proofs in
stack.rs, inert-trigger equality, several no-target `ResolvedAbility`
literals in effects/*.rs) is updated in lockstep, including two new
"must not batch a per-activation payment snapshot" gates mirroring the
existing `cost_paid_object` ones.
Third round of review on PR phase-rs#6812 (issue phase-rs#6504):

copy_spell.rs's ActivatedAbility/TriggeredAbility copy arm only called
preserve_ability_copy_source_recursive to re-stamp source_id; the rest
of the cloned ResolvedAbility chain, including the new
noted_mana_payment field, rode along unchanged. CR 707.10: a copy of an
activated ability is not itself activated, so it never paid a mana
cost — but a copied Jeweled Amulet activation still carried the
ORIGINAL's captured payment, and Effect::NoteManaSpent resolving on
the copy would falsely note colors the copy never spent.

preserve_ability_copy_source_recursive now also calls a new
ResolvedAbility::clear_noted_mana_payment_recursive (mirrors
set_noted_mana_payment_recursive's recursion shape in reverse) so the
copy's whole chain — including any sub_ability/else_ability nodes —
loses the inherited snapshot.

Added jeweled_amulet_copied_activation_does_not_note_original_payment,
which exercises the real copy pipeline (copy_spell::resolve targeting
the original activation's own stack-entry id, the same stack-entry
lookup and LIFO resolution a real "copy target activated or triggered
ability" card like Lithoform Engine drives) rather than only unit
-testing the clearing method: activates paying red, copies it,
resolves the copy first (asserting no note) and the original second
(asserting it still notes red — the paired positive reach-guard proving
the fix clears only the copy's snapshot). Verified revert-failing.
…oduction parser

Round-5 review: the intentionally unsupported "type and amount" clause
(Ice Cauldron) was only exercised by parse_note_mana_spent_clause's own
unit tests and a fabricated end-to-end sibling ("Amber Amulet") — neither
path parses Ice Cauldron's actual printed Oracle text, so its Unimplemented
status wasn't protected against an upstream routing/fallback change that
could start treating it as supported.

Added ice_cauldron_note_type_and_amount_stays_unimplemented, parsing Ice
Cauldron's full two-ability Oracle text (verified against MTGJSON) through
the production parser. It walks the exile -> cast -> counter -> note
sub-ability chain to confirm the parser actually reaches the "type and
amount" clause (not failing earlier for an unrelated reason) before
asserting it still lands on Effect::Unimplemented, and separately asserts
the second ability's "last noted type and amount of mana" is not matched
by ManaProduction::NotedType's singular-type pattern. Paired with the
existing jeweled_amulet_notes_and_reads_back_mana_type positive assertion,
this distinguishes the intentional boundary from a parser that simply
fails before reaching either clause.
@matthewevans
matthewevans force-pushed the fix/6504-jeweled-amulet-noted-mana branch from 0fc5c36 to dd95f1c Compare August 7, 2026 22:06

@matthewevans matthewevans left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔴 Blockers on current head

Reviewed dd95f1c809ed1a9e82663b39c250319e3661ba8b. This is a new review; prior review evidence does not apply to this head.

  1. Remove the prohibited dormant-importer mirror. crates/mtgish-import/src/convert/action.rs:304-319 adds ManaProduction::NotedType to rewrite_bound_x_in_mana_production. docs/AI-CONTRIBUTOR.md:91-95 explicitly prohibits modifying mtgish paths or mirroring engine variants there. This hunk must be removed or explicitly reworked under maintainer resolution; “consistency” is not a permitted reason to change a dormant importer.

    I also checked the supplied ancestry claim: 2b204dff59c1e928ab67d4683831deb8b8ff9c15 is an ancestor of 4555043a1d9c5a1f44235fd95b79588d9e12032f, but it does not contain this NotedType arm. git log -S identifies 4555043a… as the introducing commit. That provenance cannot justify retaining the policy-prohibited hunk.

  2. Bind source incarnation at payment capture, not later stack construction. casting.rs:15591-15596 records only mana types, while casting_costs.rs:5423-5429 reads the mutable object's incarnation after cost processing. The residual self-return path at casting_costs.rs:3181-3266 can move the source after automatic payment and before that late read, allowing the payment snapshot to bind to the post-zone-change incarnation.

    Capture { types, source_incarnation } atomically at the payment authority, carry that immutable snapshot through every pause/resume and stack continuation, and add a discriminating self-return-cost regression that proves the old payment cannot bind to the new incarnation.

  3. The parse-diff sticky is stale. The only <!-- coverage-parse-diff --> evidence says it was generated for 0fc5c36bfd20bf4ce40918d93b8012e31efce1eb, not this head. Current CI is still pending. After it settles, publish and reconcile a fresh parse-diff artifact bound to dd95f1c809ed1a9e82663b39c250319e3661ba8b before re-review.

@matthewevans matthewevans left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maintainer correction

This formal follow-up corrects the prior requested-changes review for unchanged head dd95f1c809ed1a9e82663b39c250319e3661ba8b.

Withdrawn item only: parse_diff_stale_for_current_head is withdrawn. The current parse-diff artifact is bound to this exact head, compares against main 1833435c0d59, and reports exactly one affected card and four signatures for Jeweled Amulet—matching the declared scope.

The requested-changes disposition remains in force for the two substantive blockers: the prohibited dormant mtgish-import mirror and late source-incarnation capture after cost processing. No approval or enqueue is implied by this correction.

@matthewevans matthewevans removed their assignment Aug 7, 2026

@matthewevans matthewevans left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Approved for merge queue.

Evidence: final head 2243c0834e has all required checks green; current-head review threads are empty; and the fresh parse-diff artifact reports the intended Jeweled Amulet change only (noted-mana production plus NoteManaSpent). The decision-cost baseline is refreshed from the exact five-sample CI report after the paired-seed gate passed.

@matthewevans
matthewevans added this pull request to the merge queue Aug 7, 2026
@matthewevans matthewevans self-assigned this Aug 8, 2026

@matthewevans matthewevans left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔴 Current-head review — approval overridden

Reviewed 2243c0834e0307f4f9c204c73cbb5f580e106e57. CI and the current parse-diff are clean, but they do not clear these material blockers:

  1. Prohibited dormant mirror remains. crates/mtgish-import/src/convert/action.rs:304-319 still adds ManaProduction::NotedType to the mtgish X-rewrite. docs/AI-CONTRIBUTOR.md:91-95 forbids modifying dormant mtgish paths or mirroring new engine variants there. Remove this hunk or obtain explicit maintainer resolution for a genuinely required exception.

  2. Source incarnation is still captured after payment and can rebind across a zone change. casting.rs:15591-15596 stores only paid types; casting_costs.rs:5423-5429 later reads obj.incarnation. The self-return residual-cost flow at casting_costs.rs:3181-3266 can move the source between those points, binding the earlier payment to the post-move incarnation.

    Capture { types, source_incarnation } atomically at the payment authority, propagate that snapshot through every pause/resume and stack continuation, and add a discriminating self-return-cost regression proving an old payment cannot bind to a new incarnation.

This changes-requested review supersedes the stale approval. No approval or enqueue pending these fixes.

@matthewevans matthewevans removed their assignment Aug 8, 2026
Merged via the queue into phase-rs:main with commit 62938c0 Aug 8, 2026
17 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bug Bug fix

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Jeweled Amulet — removes a charge without producing mana

2 participants